package com.mybatis.plugin;

import com.mybatis.util.MybatisHelper;
import com.mybatis.util.SQLUtil;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import org.apache.ibatis.scripting.xmltags.ForEachSqlNode;
import org.apache.ibatis.scripting.xmltags.MixedSqlNode;
import org.apache.ibatis.scripting.xmltags.SqlNode;
import org.apache.ibatis.scripting.xmltags.TextSqlNode;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

@Intercepts({@Signature(args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}, method = "query", type = Executor.class), 
		@Signature(args = {MappedStatement.class, Object.class}, method = "update", type = Executor.class)})
public class AutoSQLInterceptor implements Interceptor {
    public static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    public static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final List<ResultMapping> EMPTY_RESULTMAPPING = new ArrayList<ResultMapping>(0);
    private static final String _batch_sql_regex = "Batch.MapperGD.*";
    private static final String _sql_regex = ".*MapperGD.*";

    private boolean processInterceptBatch(Object[] queryArgs) throws Exception {
        MappedStatement ms = (MappedStatement) queryArgs[0];
        String mapperSQL = ms.getBoundSql(queryArgs[1]).getSql();
        if (!mapperSQL.matches(_batch_sql_regex)) {
            return false;
        }
        Class<?> entityclazz = MybatisHelper.getEntityClazz(ms.getResource());
        ArrayList<SqlNode> arrayList = null;
        ArrayList<SqlNode> arrayList2 = null;
        StringBuilder first;
        StringBuilder after;
        if ("Batch.MapperGD.insert.entity".equals(mapperSQL)) {
        	arrayList = new ArrayList<SqlNode>();
        	
            first = new StringBuilder();  // INSERT INTO tableName( field1,field2,field3,field4)  VALUES
            after = new StringBuilder(); // ( #{item.field1},#{item.field2},#{item.field3},#{item.field4}) 
            SQLUtil.insertEntityBatch(entityclazz, true, first, after);
            
            arrayList.add(new TextSqlNode(first.toString()));
            arrayList2 = new ArrayList<SqlNode>();
            arrayList2.add(new TextSqlNode(after.toString()));
            
            arrayList.add(new ForEachSqlNode(ms.getConfiguration(), new MixedSqlNode(arrayList2), "list", "index", "item", (String) null, (String) null, ","));
            arrayList.add(new TextSqlNode("  "));
            queryArgs[0] = newMappedStatement(ms, new DynamicSqlSource(ms.getConfiguration(), new MixedSqlNode(arrayList)), entityclazz);
        } else if ("Batch.MapperGD.delete.ids".equals(mapperSQL)) {
        	arrayList = new ArrayList<SqlNode>();
        	
        	first = new StringBuilder(); // "DELETE FROM tableName WHERE idField IN
            SQLUtil.deleteEntityBatch(entityclazz, first);
            
            arrayList.add(new TextSqlNode(first.toString()));
            arrayList2 = new ArrayList<SqlNode>();
            arrayList2.add(new TextSqlNode("#{item}"));
            
            arrayList.add(new ForEachSqlNode(ms.getConfiguration(), new MixedSqlNode(arrayList2), "list", "index", "item", "(", ")", ","));
            arrayList.add(new TextSqlNode("  "));
            queryArgs[0] = newMappedStatement(ms, new DynamicSqlSource(ms.getConfiguration(), new MixedSqlNode(arrayList)), entityclazz);
        } else if ("Batch.MapperGD.find.byIds".equals(mapperSQL)) {
        	arrayList = new ArrayList<SqlNode>();
        	
        	arrayList.add(new TextSqlNode(SQLUtil.selectEntityBatch(entityclazz).toString())); // SELECT field1 as field1,field2 as field2,field3 as field3 FROM tableName WHERE idField IN
            arrayList2 = new ArrayList<SqlNode>();
            arrayList2.add(new TextSqlNode("#{item}"));
            
            arrayList.add(new ForEachSqlNode(ms.getConfiguration(), new MixedSqlNode(arrayList2), "list", "index", "item", "(", ")", ","));
            arrayList.add(new TextSqlNode("  "));
            queryArgs[0] = newMappedStatement(ms, new DynamicSqlSource(ms.getConfiguration(), new MixedSqlNode(arrayList)), entityclazz);
        } else if("Batch.MapperGD.update.entity".equals(mapperSQL)){
        	arrayList = new ArrayList<SqlNode>();
        	
        	first = new StringBuilder();
        	first.append("UPDATE ").append(SQLUtil.getTableName(entityclazz));
        	arrayList.add(new TextSqlNode(first.toString()));
        	
            Field[] files = entityclazz.getDeclaredFields();
            Field field = entityclazz.getDeclaredField(SQLUtil.getTableKeyName(entityclazz));
            String idField = SQLUtil.getColumnName(field);
            String fieldName = null;
            int i = 0;
            for (Field item : files){
            	fieldName = SQLUtil.getColumnName(item);
            	if(fieldName == null || fieldName.equals(idField)){
            		continue;
            	}
                after = new StringBuilder();
                if(i == 0){
                	after.append(" SET ");
                	after.append(fieldName);
                	after.append(" = CASE ");
                	after.append(idField);
                	after.append(" ");
                	arrayList.add(new TextSqlNode(after.toString()));
                	
                	arrayList2 = new ArrayList<SqlNode>();
                    arrayList2.add(new TextSqlNode(" WHEN #{item." + idField + "} THEN #{item." + fieldName + "} "));
                    arrayList.add(new ForEachSqlNode(ms.getConfiguration(), new MixedSqlNode(arrayList2), "list", "index", "item",(String) null, "end", " "));
                    arrayList.add(new TextSqlNode("  "));
                }else{
                	after.append(" , ");
                	after.append(fieldName);
                	after.append(" = CASE ");
                	after.append(idField);
                	after.append(" ");
                	arrayList.add(new TextSqlNode(after.toString()));
                	
                	arrayList2 = new ArrayList<SqlNode>();
                    arrayList2.add(new TextSqlNode(" WHEN #{item." + idField + "} THEN #{item." + fieldName + "} "));
                    arrayList.add(new ForEachSqlNode(ms.getConfiguration(), new MixedSqlNode(arrayList2), "list", "index", "item",(String) null, "end", " "));
                    arrayList.add(new TextSqlNode("  "));
                }
                i++;
            }
            
            after = new StringBuilder();
            after.append(" WHERE ");
        	after.append(idField);
        	after.append(" IN ");
        	arrayList.add(new TextSqlNode(after.toString()));
        	
        	arrayList2 = new ArrayList<SqlNode>();
            arrayList2.add(new TextSqlNode("#{item." + idField + "}"));
            arrayList.add(new ForEachSqlNode(ms.getConfiguration(), new MixedSqlNode(arrayList2), "list", "index", "item", "(", ")", ","));
            arrayList.add(new TextSqlNode("  "));
            
            // update tableName set field1 = case idField when #{item.idField} then #{item.field1} when #{item.idField} then #{item.field1} end,field2 = case idField when #{item.idField} then #{item.field2} when #{item.idField} then #{item.field2} end where id in (#{item.idField},#{item.idField});
            queryArgs[0] = newMappedStatement(ms, new DynamicSqlSource(ms.getConfiguration(), new MixedSqlNode(arrayList)), entityclazz);
        }
        return true;
    }

    private boolean processIntercept(Object[] queryArgs) throws Exception {
        MappedStatement ms = (MappedStatement) queryArgs[0];
        Object parameter = queryArgs[1];
        String mapperSQL = ms.getBoundSql(parameter).getSql().trim();
        if (!mapperSQL.matches(_sql_regex)) {
            return false;
        }
        Class<?> entityclazz = MybatisHelper.getEntityClazz(ms.getResource());
        if (entityclazz == null) {
            throw new RuntimeException("使用公共dao必須指定接口泛型.");
        }
        String new_sql = MybatisHelper.getExecuSQL(entityclazz, mapperSQL, parameter);
        List<SqlNode> list = new ArrayList<SqlNode>();
        list.add(new TextSqlNode(new_sql));
        queryArgs[0] = newMappedStatement(ms, new DynamicSqlSource(ms.getConfiguration(), new MixedSqlNode(list)), entityclazz);
        return true;
    }

    public static MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource, Class<?> entityClazz) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), String.valueOf(ms.getId()) + "_gen", newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (!(ms.getKeyProperties() == null || ms.getKeyProperties().length == 0)) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        List<ResultMap> resultMaps = new ArrayList<ResultMap>();
        resultMaps.add(new ResultMap.Builder(ms.getConfiguration(), ms.getId(), entityClazz, EMPTY_RESULTMAPPING).build());
        builder.resultMaps(resultMaps);
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    public Object intercept(Invocation invocation) throws Throwable {
        if (!processInterceptBatch(invocation.getArgs())) {
            processIntercept(invocation.getArgs());
        }
        return invocation.proceed();
    }

    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    public void setProperties(Properties arg0) {
    }

    private class BoundSqlSqlSource implements SqlSource {
        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql2) {
            this.boundSql = boundSql2;
        }

        public BoundSql getBoundSql(Object parameterObject) {
            return this.boundSql;
        }
    }
}
